棋譜樹狀結構與分支功能改進

這是一個重大的架構改進。我們需要將原本線性的 historyFEN 陣列轉變為樹狀結構(Tree Structure),並引入一個「當前節點」的概念來追蹤玩家在棋譜樹中的位置。

以下是實現這些功能的修改方案:

1. 數據結構與全域變數

首先,定義新的數據結構並修改初始化邏輯。

JavaScript

// --- New JSON Tree Structure ---
let gameData = {
   position: INITIAL_FEN,
   move: [], // Root level moves
   comment: ""
};

// Track current position in the tree
// currentNode: reference to the move object {m, v, c}
// parentNode: reference to the array containing the current move object
let currentNode = null;
let currentPath = []; // Array of indices to reach the current move

2. 修改 pushHistory 處理分支邏輯

當走棋時,根據目前是否處於該分支的末尾來決定是「追加」還是「新增分支」。

JavaScript

function pushHistory(moveText) {
   const newMove = { m: moveText };
   let targetList;

   // Determine where to insert the new move
   if (currentStepIndex === -1) {
       // We are at the starting position
       targetList = gameData.move;
   } else {
       // We are at some move node, new moves go into its 'v' (variations)
       if (!currentNode.v) currentNode.v = [];
       targetList = currentNode.v;
   }

   // Check if this move already exists in the variations
   let existingMove = targetList.find(item => item.m === moveText);

   if (!existingMove) {
       // Add as a new branch/variation
       targetList.push(newMove);
       currentNode = newMove;
   } else {
       // Move already exists, just switch to that branch
       currentNode = existingMove;
   }

   // Update path and indices
   // (Logic to update currentPath and sync with UI...)
   renderRecordUI();
}

3. 分支標記 (.branch-marker) 與 CSS 調整

在 style 標籤中加入分支標記與選單的樣式。

CSS

/* --- Branch Marker Styling --- */
.branch-marker {
   position: absolute;
   right: 5px;
   top: 50%;
   transform: translateY(-50%);
   background-color: #8b0000; /* Dark Red */
   color: white;
   width: 24px;
   height: 24px;
   border-radius: 50%;
   font-size: 9px;
   display: flex;
   justify-content: center;
   align-items: center;
   cursor: pointer;
   z-index: 10;
}

/* --- Branch Dropdown Menu --- */
.branch-menu {
   position: absolute;
   background: #444;
   border: 1px solid #666;
   box-shadow: 0 4px 8px rgba(0,0,0,0.5);
   z-index: 100;
   min-width: 150px;
   padding: 5px 0;
}

.branch-menu-item {
   display: flex;
   align-items: center;
   padding: 5px 10px;
   gap: 10px;
   cursor: pointer;
}

.branch-menu-item.active { background: #3d5afe; }
.branch-menu-item:hover { background: #555; }
.btn-del-branch { color: #ff5252; margin-left: auto; }

4. 分支選單邏輯(切換、刪除與排序)

這部分處理點擊標記後的彈出選單以及「序號點擊排序」的特殊邏輯。

JavaScript

let selectedSortIdx = null;

function showBranchMenu(anchorEl, variations, currentIndex) {
   const menu = document.createElement('div');
   menu.className = 'branch-menu';
   
   variations.forEach((v, i) => {
       const item = document.createElement('div');
       item.className = `branch-menu-item ${i === currentIndex ? 'active' : ''}`;
       
       // Index Label (for sorting)
       const idxLabel = document.createElement('span');
       idxLabel.textContent = i + 1;
       idxLabel.onclick = (e) => {
           e.stopPropagation();
           handleBranchSort(variations, i);
       };

       // Move Text
       const moveTxt = document.createElement('span');
       moveTxt.textContent = v.m;
       moveTxt.onclick = () => { /* Switch to this branch */ };

       // Delete Button
       const delBtn = document.createElement('span');
       delBtn.innerHTML = '×';
       delBtn.className = 'btn-del-branch';
       delBtn.onclick = (e) => {
           e.stopPropagation();
           variations.splice(i, 1);
           renderRecordUI();
       };

       item.append(idxLabel, moveTxt, delBtn);
       menu.appendChild(item);
   });
   
   // Position menu below anchorEl
   document.body.appendChild(menu);
}

function handleBranchSort(variations, clickedIdx) {
   if (selectedSortIdx === null) {
       selectedSortIdx = clickedIdx;
       // Visual feedback for selection...
   } else {
       if (selectedSortIdx !== clickedIdx) {
           const element = variations.splice(selectedSortIdx, 1)[0];
           // If x < y, move x below y. If x > y, move x above y.
           variations.splice(clickedIdx, 0, element);
       }
       selectedSortIdx = null;
       renderRecordUI();
   }
}

5. 文件存取 (Save & Open)

實現 JSON 下載與讀取功能。

JavaScript

// --- Tool Save ---
document.getElementById('tool-save').addEventListener('click', () => {
   const timestamp = new Date().toISOString().replace(/[-T:Z]/g, '').slice(0, 14);
   const fileName = `ejcees_json_${timestamp}.txt`;
   const blob = new Blob([JSON.stringify(gameData, null, 2)], { type: 'text/plain' });
   
   const a = document.createElement('a');
   a.href = URL.createObjectURL(blob);
   a.download = fileName;
   a.click();
});

// --- Tool Open ---
document.getElementById('tool-open').addEventListener('click', () => {
   const input = document.createElement('input');
   input.type = 'file';
   input.accept = '.txt';
   input.onchange = (e) => {
       const file = e.target.files[0];
       const reader = new FileReader();
       reader.onload = (event) => {
           try {
               gameData = JSON.parse(event.target.result);
               // Reset state and jump to start
               currentStepIndex = -1;
               loadFEN(gameData.position);
               renderRecordUI();
           } catch (err) {
               alert("Invalid JSON format");
           }
       };
       reader.readAsText(file);
   };
   input.click();
});

總結所需修改:

  1. 數據模型:廢棄 historyFEN 陣列,改用遞迴結構的 gameData。
  2. UI 渲染:renderRecordUI 現在必須遞迴地尋找當前路徑上的 v 列表。
  3. 互動邏輯
  • 棋子移動後,檢查 currentNode.v 是否已有該走法。
  • .branch-marker 點擊時生成 branch-menu。
  • 選單內的序號點擊需實作兩次點擊的位移邏輯。
  1. 存檔格式:透過 JSON.stringify 確保不含空的 v 或 c 欄位(可在存檔前用遞迴清理空屬性)。

這套改進將使你的棋盤程式具備專業的研究功能,支援多重分支與導出標準棋譜。